﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Win32;

namespace Red.Scripts.Defines.Utilities
{
    /// <summary>
    /// Helpful class for finding Red Engine files
    /// </summary>emen
    public static class Locator
    {
        #region Public Types

        public class Data
        {
            #region Public Properties

            public string Filename { get; set; }

            public string OriginalDirectory { get; set; }

            public IEnumerable<SccSearchOrder> SearchOrder { get; set; }

            public string CustomFilePath { get; set; }

            public HashSet<string> Ignore { get; set; }

            public bool SearchHierarchy { get; set; } = true;

            #endregion Public Properties

            #region Internal Properties

            internal HashSet<string> Used { get; private set; } = new HashSet<string>();
            internal IEnumerator<SccSearchOrder> CurrentsearchPathIterator { get; set; }
            internal string CurrentDirectory { get; set; }

            internal SccSearchOrder CurrentsearchPath => CurrentsearchPathIterator.Current;

            #endregion Internal Properties

            #region Internal Methods

            internal void Defaults()
            {
                if ( SearchOrder == null )
                {
                    SearchOrder = (IEnumerable<SccSearchOrder>)Enum.GetValues( typeof( SccSearchOrder ) );
                }

                if ( Ignore == null )
                {
                    Ignore = new HashSet<string> ();
                }

                if ( OriginalDirectory == null )
                {
                    OriginalDirectory = Directory.GetCurrentDirectory();
                }
            }

            internal bool NextSearchPath()
            {
                System.Diagnostics.Debug.Assert( SearchOrder != null );

                if ( CurrentsearchPathIterator == null )
                    CurrentsearchPathIterator = SearchOrder.GetEnumerator();

                if ( !CurrentsearchPathIterator.MoveNext() )
                {
                    CurrentsearchPathIterator = SearchOrder.GetEnumerator();
                    return false;
                }

                return true;
            }

            internal bool NextDirectory()
            {
                if ( CurrentDirectory == null )
                    CurrentDirectory = OriginalDirectory;
                else if( !SearchHierarchy )
                    CurrentDirectory = null;
                else
                    CurrentDirectory = Directory.GetParent( CurrentDirectory )?.FullName;

                return CurrentDirectory != null;
            }

            #endregion Internal Methods
        }

        #endregion Public Types

        #region Public Properties

        /// <summary>
        /// Path to the currently set as active red engine package
        /// </summary>
        public static string ActiveEnginePackagePath => GetActiveEnginePackagePath();
        public static string ActiveWorkspaceRoot => GetActiveWorkspaceRoot();

        #endregion Public Properties

        #region Public Methods

        public static string LocateBasic( string filename, IEnumerable<SccSearchOrder> searchOrder )
        {
            Data data = new Data()
            {
                Filename = filename,
                SearchOrder = searchOrder
            };

            return Locate( data );
        }

        /// <summary>
        /// Search for a file in a red engine package
        /// </summary>
        /// <returns>The full path to the file, or null if not found</returns>
        public static string Locate( Data data )
        {
            data.Defaults();

            while ( data.NextSearchPath() )
            {
                string path = ScanDirectoryHierarchy( data );

                if ( path != null )
                    return path;
            }

            return null;
        }

        #endregion Public Methods

        #region Private Methods

        private static string ScanDirectoryHierarchy( Data data )
        {
            while ( data.NextDirectory() )
            {
                string path;
                if ( ScanDirectory( data, out path ) )
                    return path;
            }

            return null;
        }

        private static bool ScanDirectory( Data data, out string foundPath )
        {
            foundPath = null;

            string path = ResolvePath( data );

            if ( path == null )
                return false;

            if ( !File.Exists( path ) )
                return false;

            // Path marked ignore by user?
            if ( data.Ignore.Contains( path ) )
                return false;

            // Path already checked by us?
            if ( data.Used.Contains( path ) )
                return false;

            // Don't check this path a second time
            data.Used.Add( path );

            foundPath = path;

            return true;
        }

        private static string SubLocate( Data data, string customPath )
        {
            if ( customPath == null )
                return null;

            // Only resolve custom path searches if we're at the top of the directory heirarchy
            if ( data.CurrentDirectory != data.OriginalDirectory )
                return null;

            Data customData = new Data()
            {
                Filename = data.Filename,
                SearchOrder = from item in data.SearchOrder where item != SccSearchOrder.Package select item,
                OriginalDirectory = customPath,
                SearchHierarchy = false,
                CustomFilePath = data.CustomFilePath
            };

            string found = Locate( customData );

            return found;
        }

        private static string ResolvePath( string filename, SccSearchOrder searchPath, string currentDirectory )
        {
            System.Diagnostics.Debug.Assert( currentDirectory != null );

            switch ( searchPath )
            {
                case SccSearchOrder.BinDebugDll:
                    return Path.Combine( currentDirectory, "bin", "x64_DLL.Debug", filename );

                case SccSearchOrder.BinDebug:
                    return Path.Combine( currentDirectory, "bin", "x64.Debug", filename );

                case SccSearchOrder.BinReleaseDll:
                    return Path.Combine( currentDirectory, "bin", "x64_DLL.Release", filename );

                case SccSearchOrder.BinRelease:
                    return Path.Combine( currentDirectory, "bin", "x64.Release", filename );

                case SccSearchOrder.BinFinalDLL:
                    return Path.Combine( currentDirectory, "bin", "x64_DLL.Final", filename );

                case SccSearchOrder.BinFinal:
                    return Path.Combine( currentDirectory, "bin", "x64.Final", filename );

                case SccSearchOrder.EngineTools:
                    return Path.Combine( currentDirectory, "engine", "tools", filename );

                case SccSearchOrder.Custom:
                    if ( !ValidateCustomPath( currentDirectory ) )
                        return null;

                    return Path.Combine( Path.GetDirectoryName( currentDirectory ), filename );
            }

            return null;
        }

        private static bool ValidateCustomPath( string path )
        {
            if ( string.IsNullOrWhiteSpace( path ) )
                return false;

            try
            {
                Path.GetFullPath( path );

                return true;
            }
            catch
            {
                return false;
            }
        }

        private static string ResolvePath( Data data )
        {
            switch ( data.CurrentsearchPath )
            {
                case SccSearchOrder.Package:
                    return SubLocate( data, ActiveEnginePackagePath );

                case SccSearchOrder.Custom:
                    if ( !string.IsNullOrWhiteSpace( data.CustomFilePath ) )
                        return ResolvePath( data.Filename, data.CurrentsearchPath, data.CustomFilePath );
                    else
                        return null;

                default:
                    return ResolvePath( data.Filename, data.CurrentsearchPath, data.CurrentDirectory );
            }
        }

        private static string LocateDirectory( string directory )
        {
            string root = Directory.GetCurrentDirectory();

            while ( root != null )
            {
                string path = Path.Combine( root, directory );

                if ( Directory.Exists( path ) )
                    return path;

                root = Directory.GetParent( root )?.FullName;
            }

            return null;
        }

        private static RegistryKey GetRegistryKeysRoot()
        {
            RegistryKey environmentKey = Registry.CurrentUser.OpenSubKey( "Software" );
            return environmentKey?.OpenSubKey( "CDProjektRED" );
        }

        private static string GetActiveEnginePackagePath()
        {
            RegistryKey environmentKey = GetRegistryKeysRoot();

            if ( environmentKey == null )
                return null;

            string executorPackagePath = environmentKey.GetValue( @"EX_ENGINE_PATH_PACKAGE" ) as string;

            if ( !string.IsNullOrWhiteSpace( executorPackagePath ) && Directory.Exists( executorPackagePath ) )
                return executorPackagePath;

            string redLauncherPackagePath = environmentKey.GetValue( @"RL_ENGINE_PATH_DIR" ) as string;
            return redLauncherPackagePath;
        }

        private static string GetActiveWorkspaceRoot()
        {
            RegistryKey environmentKey = GetRegistryKeysRoot();

            if ( environmentKey == null )
                return null;

            string executorActiveWorkspaceRoot = environmentKey.GetValue( "EX_WS_ROOT" ) as string;

            if ( !string.IsNullOrWhiteSpace( executorActiveWorkspaceRoot ) )
                return executorActiveWorkspaceRoot;

            string subdirsToFind = Path.Combine( "r6", "depot", "base" );

            string foundPath = LocateDirectory( subdirsToFind );

            if ( string.IsNullOrWhiteSpace( foundPath ) )
                return null;

            // We need to trim the searched for subdirs, as they're not actually part of this path
            // but the root path that contains them
            return foundPath.Substring( 0, foundPath.Length - subdirsToFind.Length );
        }

        #endregion Private Methods
    }
}
